Skip to content

api: disallow a guest to change his role to member/manager#7740

Merged
BacLuc merged 1 commit intoecamp:develfrom
BacLuc:fix-permissions-collaboration
Jul 22, 2025
Merged

api: disallow a guest to change his role to member/manager#7740
BacLuc merged 1 commit intoecamp:develfrom
BacLuc:fix-permissions-collaboration

Conversation

@BacLuc
Copy link
Contributor

@BacLuc BacLuc commented Jul 9, 2025

Fix it in CampCollaborationUpdateProcessor for now, because that's the only place this is a problem now.
Maybe we should later fix this in the CampRoleVoter. It only is a problem when the own campCollaboration is changed.

@BacLuc BacLuc requested a review from a team July 9, 2025 17:06
@BacLuc
Copy link
Contributor Author

BacLuc commented Jul 9, 2025

@CodeRabbit review this pr

@coderabbitai
Copy link

coderabbitai bot commented Jul 9, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll help you review this PR! Let me gather information about the changes first.


🏁 Script executed:

#!/bin/bash
# Get PR information using GitHub CLI
gh pr view 7740 --repo ecamp/ecamp3 --json title,body,files,additions,deletions,changedFiles,baseRefName,headRefName,author,state,mergeable

# Get the diff for the PR
gh pr diff 7740 --repo ecamp/ecamp3

Length of output: 16736


@BacLuc Great work on implementing this security fix! This PR effectively prevents privilege escalation by guests. Here's my comprehensive review:

Security Implementation

The core security logic is solid:

  • Guests cannot elevate their roles to member/manager (blocks privilege escalation)
  • Members/managers can still downgrade their roles (preserves legitimate functionality)
  • Only applies to users modifying their own collaborations (other permissions handled by voter)

🔧 Code Quality Issues

  1. Unused Dependency: CampRoleVoter is injected but never used in the code:
private readonly CampRoleVoter $campRoleVoter, // ← Remove this
  1. Unclear Error Message: The generic "Not authorized to change role" could be more specific:
throw new HttpException(403, 'Guests cannot elevate their role to member or manager');
  1. Code Clarity: The logic could be more readable:
public function onBeforeRoleChange(CampCollaboration $data, CampCollaboration $previous): CampCollaboration {
    $currentUser = $this->security->getUser();
    $isOwnCollaboration = $data->user->getId() === $currentUser->getId();
    
    // Only check role escalation for own collaborations
    if (!$isOwnCollaboration) {
        return $data;
    }
    
    $previousRole = $previous->role;
    $canElevateRole = in_array($previousRole, [CampCollaboration::ROLE_MANAGER, CampCollaboration::ROLE_MEMBER], true);
    
    if (!$canElevateRole) {
        throw new HttpException(403, 'Guests cannot elevate their role to member or manager');
    }
    
    return $data;
}

📋 Testing Coverage

Excellent test coverage! The tests cover:

  • ✅ Guest role escalation prevention
  • ✅ Member/manager role changes
  • ✅ Status changes
  • ✅ Combined status/role changes

Minor improvement - some test method names could be more descriptive:

// Instead of:
testPatchOwnCampCollaborationToMemberIsRejectedForGuest()
// Consider:
testGuestCannotElevateOwnRoleToMember()

🏗️ Architecture Notes

The PropertyChangeListener pattern is well-implemented. The changes to support 1-2 parameters in PropertyChangeListener.php are necessary and correctly implemented.

Your note about potentially moving this to CampRoleVoter later is valid - this would centralize authorization logic, but the current approach works well for the immediate need.

🚀 Overall Assessment

This is a solid security improvement with good test coverage. The main issues are minor code quality improvements (unused dependency, clearer error messages) rather than functional problems.

Recommendation: ✅ Approve after addressing the unused dependency.

🐰

   (\   /)
  ( ._. )
 o_(")(")

Fix it in CampCollaborationUpdateProcessor for now, because that's
the only place this is a problem now.
Maybe we should later fix this in the CampRoleVoter.
It only is a problem when the own campCollaboration is changed.
@BacLuc BacLuc force-pushed the fix-permissions-collaboration branch from 7af3f32 to af31650 Compare July 9, 2025 17:27
@BacLuc
Copy link
Contributor Author

BacLuc commented Jul 9, 2025

@CodeRabbit i fixed some issues

@coderabbitai
Copy link

coderabbitai bot commented Jul 9, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the latest PR diff to see current changes
gh pr diff 7740 --repo ecamp/ecamp3

# Get current PR status
gh pr view 7740 --repo ecamp/ecamp3 --json title,body,files,additions,deletions,changedFiles,commits,updatedAt

Length of output: 16753


@BacLuc Excellent work on the updates! I can see you've addressed several key issues from my previous review:

Fixed Issues

  1. Unused Dependency Removed: The CampRoleVoter dependency has been properly removed from the constructor - great catch!

  2. Proper Imports Added: Added the necessary User and HttpException imports that were missing.

  3. Significantly Expanded Test Coverage: Outstanding improvement! The test suite now covers:

    • Guest role escalation prevention (both to member and manager)
    • Member role changes (including elevation to manager)
    • Manager role changes (including demotion)
    • Status changes for different roles
    • Combined status and role changes
    • Much more comprehensive edge cases
  4. Test Code Improvements: Fixed the test method calls to use getFixture() instead of static::$fixtures for better consistency.

  5. PropertyChangeListener Enhancement: Properly implemented support for 1-2 parameters as needed.

💡 Minor Remaining Opportunities

The security implementation is solid and functional! A couple of minor refinements could still be considered:

  • Error Message: Still shows the generic "Not authorized to change role" - a more specific message like "Guests cannot elevate their role to member or manager" would be clearer for users.

  • Code Readability: The logic works perfectly but could be slightly more readable with variable names that make the intent clearer.

🎯 Overall Assessment

This is a significant improvement! The expanded test coverage is particularly impressive and gives great confidence in the security fix. The main functional issues have been resolved.

Status: ✅ Ready for merge - the security implementation is solid and well-tested.

Great work on addressing the feedback so thoroughly! 🚀

@pmattmann pmattmann requested a review from a team July 14, 2025 20:40
if ($data->user->getId() !== $user->getId()) {
return $data;
}
if (in_array($previous->role, [CampCollaboration::ROLE_MANAGER, CampCollaboration::ROLE_MEMBER], true)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Members also aren't allowed to change their own role, or are they? Doesn't the same apply for members?
And is there some camp collaboration state which has the same flaw?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are with the current implementation.
A similar issue: #3440
https://github.com/ecamp/ecamp3/blob/devel/api/src/Entity/Camp.php#L39
https://github.com/ecamp/ecamp3/blob/devel/api/src/Entity/CampCollaboration.php#L44

I did not wan't to change that too in the same PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this PR is only concerned with restricting guests, not members. I'm fine with that, as long as we don't keep the equivalent flaw for members around for long.

}
if (self::hasOneParameter($beforeAction)) {
throw new \InvalidArgumentException('afterAction must have exactly one parameter');
if (self::hasAtMostTwoParameter($beforeAction)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming of these parameter checking functions seems backwards to me. It's not returning whether the callback has at most two parameters, it's returning whether it has more than two or less than one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, i also ran into it when developing.
But as far as i remember php-cs-fixer did not like !self::hasOneParameter because the method would only be used with negation.
Should i change this in this PR?

e.g.
validationFailsFoOneParamter
validationFailsForAtMostTwoParamter
?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you. Since you had to rewrite the PropertyChangeListener for this feature anyways, I'd have no problem accepting this refactoring in the same PR. But we can also do it later to get things moving here.

Copy link
Member

@carlobeltrame carlobeltrame left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's okay for me to merge now, to fix this one case. However I'd like to have a more general fix in the voter some time. The voter could load the original data of $subject before the change from the db and perform the permission check on that old version of the object. Or, to gain access to the request context, like AssertAllowTransitionsValidator, the Voter could get a RequestStack object and then get the old version of the subject via $this->requestStack->getCurrentRequest()->attributes->get('previous_data') (but maybe make sure the subject is really equivalent to the entity being mutated, by checking iri equality)

@BacLuc BacLuc added this pull request to the merge queue Jul 22, 2025
Merged via the queue into ecamp:devel with commit c29d72a Jul 22, 2025
30 checks passed
@BacLuc BacLuc deleted the fix-permissions-collaboration branch July 22, 2025 18:46
@carlobeltrame carlobeltrame mentioned this pull request Jul 22, 2025
@BacLuc
Copy link
Contributor Author

BacLuc commented Sep 6, 2025

It's okay for me to merge now, to fix this one case. However I'd like to have a more general fix in the voter some time. The voter could load the original data of $subject before the change from the db and perform the permission check on that old version of the object. Or, to gain access to the request context, like AssertAllowTransitionsValidator, the Voter could get a RequestStack object and then get the old version of the subject via $this->requestStack->getCurrentRequest()->attributes->get('previous_data') (but maybe make sure the subject is really equivalent to the entity being mutated, by checking iri equality)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants